namespace RL

open Garnet.Composition
open RL.Action
open RL.Components
open RL.FieldOfVision
open RL.Mapping
open RL.Pathfinding

module AI =
    type BaseAI() =
        member val Target : Option<Eid> = None with get,set
        member val TargetDmap : Option<DijkstraMap> = None with get,set

    let DetermineAIAction(c : Container, entity : Entity) =
        let mutable determinedAction = { actionType = ActionType.ShortWait; cost = 25; initialCost = 25 }

        let pos = entity.Get<Point2D>()
        let ai = entity.Get<BaseAI>()
        let fov = entity.Get<FieldOfVision>()
        let action = entity.Get<Action>()

        // Set the AI's target if it doesn't have one yet.
        if ai.Target.IsNone then
            for e in c.Query<Eid, Player, Point2D>() do
                if fov.PointSet |> List.contains e.Value3 then
                    ai.Target <- Some(e.Value1)

        // Only process if we have a target.
        if ai.Target.IsSome then
            let target = c.Get(ai.Target.Value)
            // Modify the target's position if they're moving.
            let t_pos =
                let t_act = target.Get<Action>()
                match t_act.actionType with
                    | ActionType.Move(d) -> d
                    | _ -> target.Get<Point2D>()

            let distance = Helpers.distance2D(pos.x, t_pos.x, pos.y, t_pos.y)
            // Lose the target if it ends up outside the FOV.
            if not (fov.PointSet |> List.contains t_pos) then
                ai.Target <- None
            else
                // Attack if we're within one tile of the target.
                if distance <= 1.45f then
                    determinedAction <- ActionDefs.Attack target.Id
                // Otherwise, figure out how far we need to move.
                else
                    let result, map : bool * RLMap = c.TryGetResource("Map")
                    if result then
                        // Set up a new Dijkstra map if we need to. Otherwise clear the current one.
                        if ai.TargetDmap.IsNone then
                            ai.TargetDmap <- Some(DijkstraMap(map.Width, map.Height, 40.0f))
                        else
                            ai.TargetDmap.Value.Clear()
                        let dmap = ai.TargetDmap.Value

                        let blockList =
                            GetBlockingPositionsExcept(c, ai.Target.Value)
                            |> List.map (fun p -> map.Index p.x p.y)
                        // Rebuild the Dijkstra map to path to our target position.
                        // Then move to the nearest exiting tile (if possible).
                        dmap.Build([(map.Index t_pos.x t_pos.y)], blockList, map)
                        let exit = dmap.FindLowestExit((map.Index pos.x pos.y), blockList, map)
                        if exit.IsSome then
                            let exit, _ = exit.Value
                            determinedAction <- ActionDefs.Move (map.ReverseIndex exit)

        // Final pass to see if we're changing our course of action. If so, reduce the cost of this new action.
        match action.actionType with
            | ActionType.Move(_) | ActionType.Attack(_) ->
                determinedAction <- { actionType = determinedAction.actionType;
                                    cost = determinedAction.cost - min action.cost action.initialCost;
                                    initialCost = determinedAction.initialCost}
            | _ -> ()
        // Return the new action.
        determinedAction

    type Container with
        member c.RegisterAISystem() =
            c.On<AIProcessActionTrigger> <| fun r ->
                let entity = r.e
                if entity.Has<BaseAI>() then
                    entity.Set(DetermineAIAction(c, entity))

    let init(c : Container) =
        Disposable.Create[c.RegisterAISystem()] |> ignore
